package com.googlecode.cswish.struts.spring;

import java.lang.reflect.Method;
import java.util.List;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.ServletContext;

import net.sf.ehcache.Ehcache;
import net.sf.ehcache.CacheManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.cache.spi.CacheKey;
import org.hibernate.ejb.HibernateQuery;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.type.IntegerType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.struts.IStartupManager;
import com.googlecode.cswish.struts.hibernate.HibernateConfiguration;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.bean.ModelUtil;

@Service
public class CacheSearchInitService implements IStartupManager, ICacheSearchInitService {

	private static final Log logger = LogFactory.getLog(CacheSearchInitService.class);
	
	@Resource
	private GenericService genericService;
	
	@Resource
	private CacheSearchService cacheSearchService;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private HibernateConfiguration hibernateConfiguration;
	
	@PersistenceContext(unitName="espirit")
	protected EntityManager em;

	public void forceLoadSecondLevelCache() {
		preLoadSecondLevelCache(0, 10000);
	}
	
	public void refreshCache(String name) {
		Integer id = cacheSearchService.getMaxCacheId(name);
		if (id != null) {
			CacheManager cacheManager = genericService.getCacheManager();
			Ehcache cache = cacheManager.getEhcache(name);
			Class clazz = ClassUtil.getClass(name);
			loadCache(0, id, 0, clazz, cache);
		}
	}
	
	@SuppressWarnings("rawtypes")
	private void preLoadSecondLevelCache(int waitTime, int maxEnableCacheCount) {
		final CacheManager cacheManager = genericService.getCacheManager();
		
		Executor cacheUpdate = new Executor() {
			@Override
			public Object execute(Object parameters) {
				Object[] params = (Object[])parameters;
				Object newModel = params[0];
				Object oldModel = params[1];
				String operation = (String)params[2];
				
				String modelName;
				Integer id;
				if (oldModel == null) {
					modelName = ClassUtil.getClassName(newModel);
					id = (Integer) ModelUtil.getSimpleProperty(newModel, "id");
				} else {
					modelName = ClassUtil.getClassName(oldModel);
					id = (Integer) ModelUtil.getSimpleProperty(oldModel, "id");
				}			
				
				// TODO: transaction issue, update the cache directly? (another guard is CacheSearchService.checkAndGetPercentage)
				try {
					// any other way to ensure this method runs after UpdateTimestampsCache.invalidate?
					Thread.sleep(100);
				} catch (Exception ex) {
					logger.debug("Sleep error");
				}
				
				Ehcache cache = getCache(cacheManager, modelName);
				int changeCount;
				// update the cache
				if (GenericService.OPERATION_DELETE.equals(operation)) {
					SessionFactoryImpl factory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
					CacheKey cacheKey = new CacheKey(id, IntegerType.INSTANCE, modelName, null, factory);
					cache.remove(cacheKey);
					changeCount = -1;
				} else {				// GenericService.OPERATION_MERGE
					changeCount = oldModel == null ? 1 : 0;
					// force EntityRegionAccessStrategy.putFromLoad to execute
					genericService.load(ClassUtil.forName(modelName), id);
				}
				
				// update the timestamp
				Integer maxId = cacheSearchService.getMaxCacheId(cache.getName());
				if (maxId == null || maxId < id) {
					maxId = id;
				}
				Integer totalCount = cacheSearchService.getTotalCount(cache.getName());
				if (totalCount != null) {
					totalCount += changeCount;
				}
				cacheSearchService.setCacheInfo(cache.getName(), maxId, null, totalCount);
				return null;
			}
		};
		
		for (String name : cacheManager.getCacheNames()) {
			Ehcache cache = cacheManager.getEhcache(name);
			Class clazz = ClassUtil.getClass(name);
			if (cache.isSearchable() && clazz != null) {
				genericService.on(clazz, "merge,delete", cacheUpdate, false);
				List<Class> subClasses = beanIntrospector.getSubClass(clazz.getName());
				if (subClasses != null) {
					for (Class subClazz : subClasses) {
						genericService.on(subClazz, "merge,delete", cacheUpdate, false);
					}
				}
				
				loadCache(waitTime, 0, maxEnableCacheCount, clazz, cache);
			}
		}
		logger.debug("Cache Initialization done");
	}
	
	private Ehcache getCache(CacheManager cacheManager, String modelName) {
		Class clazz = ClassUtil.getClass(modelName);
		Ehcache cache = cacheManager.getEhcache(clazz.getName());
		
		if (cache == null) {
			Class checkClazz = clazz.getSuperclass();
			
			while (cache == null && checkClazz != null) {
				cache = cacheManager.getEhcache(checkClazz.getName());
				checkClazz = checkClazz.getSuperclass();
			}
		}
		return cache;
	}

	private void loadCache(int waitTime, int beginId, int maxEnableCacheCount, Class clazz, Ehcache cache) {
		String name = clazz.getName();
		String ql = "select a from " + name + " a";
		if (beginId > 0) {
			ql += " where id > " + beginId;
		}
		int dbTotalCount = genericService.getCount(new QLInfo(ql, true), false);
		
		HibernateQuery hibernateQuery = (HibernateQuery)em.createQuery(ql);
		org.hibernate.Query query = hibernateQuery.getHibernateQuery();
		int maxElementSize = cache.getCacheConfiguration().getMaxElementsInMemory();
		if (maxEnableCacheCount != 0 && maxElementSize > maxEnableCacheCount) {
			maxElementSize = maxEnableCacheCount;
		}
		if (maxElementSize > 0) {
			query.setMaxResults(maxElementSize + 1);
		}
		int totalCount;
		// identify the 'name' is cache object, but it's stills loading
		if (beginId == 0) {
			totalCount = 0;
			cacheSearchService.setCacheInfo(name, 0, 0, 0);
		} else {
			totalCount = cacheSearchService.getTotalCount(name);
		}
		
		Method idRead = null;
		try {
			idRead = clazz.getMethod("getId");
		} catch (Exception ex) {
			logger.error("Get id", ex);
		}
		ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
		boolean ret = true;
		Object val = null;
		while (ret) {
			try {
				ret = results.next();
				if (ret) {
					val = results.get(0);
				}
			} catch (Exception ex) {
				ret = !results.isLast();
				results.scroll(1);
				logger.error("Wrong entity", ex);
			}
			totalCount++;
			
			if (totalCount % 1000 == 0) {
				Integer id = null;
				try {
					// update the max id
					id = (Integer)idRead.invoke(val);
				} catch (Exception ex) {
					logger.error("Get id", ex);
				}
				int percentage = (totalCount * 100 / dbTotalCount);
				if (percentage > 99) {		// has data, can't set it to 100%
					percentage = 99;
				}
				cacheSearchService.setCacheInfo(name, id, percentage, totalCount);
				
				if (waitTime > 0) {
					try {
						// make room for the other jobs
						Thread.sleep(waitTime);
					} catch (InterruptedException e) {
						break;
					}
				}
			}
		}
		
		/*
		Iterator iter = query.iterate();
		while (iter.hasNext()) {
			try {
				iter.next();		// force load the second level cache
			} catch (Exception ex) {
				logger.error("Wrong entity", ex);
			}
			count++;
			
			if (waitTime > 0 && count % 100 == 0) {
				try {
					// make room for the other jobs
					Thread.sleep(waitTime);
				} catch (InterruptedException e) {
					break;
				}
			}
		}*/
		
		Integer id = null;
		if (val != null) {
			try {
				// update the max id
				id = (Integer)idRead.invoke(val);
			} catch (Exception ex) {
				logger.error("Get id", ex);
			}
		}
		// all data loaded
		Integer percentage = null;
		if (maxElementSize <= 0 || totalCount < maxElementSize) {
			percentage = 100;
		}
		cacheSearchService.setCacheInfo(name, id, percentage, totalCount);
		// TODO: check the new record again?
	}
	
	@Async
	@Override
	public void startup(ServletContext servletContext) {
		/*
		boolean isJunitEnv = "true".equals(System.getProperty(SystemEnv.JUNIT_TEST));
		if (!isJunitEnv) {
			boolean isEclipseEnv = "true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE));
			int waitTime = isEclipseEnv ? 10000 : 100;
			int maxEnableCacheCount = isEclipseEnv ? 10000 : 0;
			preLoadSecondLevelCache(waitTime, maxEnableCacheCount);
		}
		*/
	}
}
